// MyFamily - XML based genealogy program.
// Copyright (C) 2006  Doug Swisher
// See the file COPYING.txt for license details.

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.IO;


namespace DataModel
{
    public class FamilyInfo
    {
        private Dictionary<string, FamilyPersona> personas = new Dictionary<string, FamilyPersona>();
        private Dictionary<string, FamilyIndividual> individuals = new Dictionary<string, FamilyIndividual>();
        private Dictionary<string, FamilySource> sources = new Dictionary<string, FamilySource>();
        private Dictionary<string, FamilyExtract> extracts = new Dictionary<string, FamilyExtract>();


        private FamilyInfo()
        {
        }



        private void LoadFamilyFromGedCom(GedComNode node, GedComImportResults results)
        {
            FamilyPersona father = null;
            FamilyPersona mother = null;

            foreach (GedComNode childNode in node.Children)
            {
                switch (childNode.Tag)
                {
                    case "CHAN":
                        // TODO - Doug - 8/1/2006 - Figure out what to do with FAM CHAN nodes
                        break;

                    case "CHIL":
                        FamilyPersona child = this.personas[childNode.Value];

                        if (father != null)
                        {
                            child.Father = father;
                        }

                        if (mother != null)
                        {
                            child.Mother = mother;
                        }
                        break;

                    case "DIV":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with FAM DIV nodes
                        break;

                    case "HUSB":
                        father = this.personas[childNode.Value];
                        break;

                    case "MARR":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with FAM MARR nodes
                        break;

                    case "NOTE":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with FAM NOTE nodes
                        break;

                    case "SLGS":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with FAM SLGS nodes
                        break;

                    case "WIFE":
                        mother = this.personas[childNode.Value];
                        break;

                    default:
                        results.LogUnhandledTag(node, childNode);
                        break;
                }
            }
        }


        public static FamilyInfo LoadFromGedCom(GedComFile ged, GedComImportSettings settings, GedComImportResults results)
        {
            FamilyInfo family = new FamilyInfo();
            bool sawTrailer = false;
            bool sawHeader = false;

            // TODO - Doug - 8/6/2006 - Build a better name for this source; build a better extract, too!
            FamilySource gedSource = new FamilySource("ged");
            gedSource.Title = "GedCom File";
            family.sources.Add(gedSource.Id, gedSource);

            FamilyExtract gedExtract = new FamilyExtract("ged", gedSource);
            family.extracts.Add(gedExtract.Id, gedExtract);

            foreach (GedComNode node in ged.Children)
            {
                switch (node.Tag)
                {
                    case "FAM":
                        family.LoadFamilyFromGedCom(node, results);
                        break;

                    case "HEAD":
                        // TODO - Doug - 8/5/2006 - parse info out of the top-level HEAD tag and use it to populate the gedSource
                        sawHeader = true;
                        break;

                    case "INDI":
                        // TODO - Doug - 8/6/2006 - one indi can create multiple personas, depending on how many sources there are...
                        FamilyPersona persona = FamilyPersona.LoadFromGedCom(node, results, gedExtract);
                        family.personas.Add(persona.Id, persona);
                        gedExtract.AddPersona(persona);

                        if (settings.CreateIndividuals)
                        {
                            FamilyIndividual individual = new FamilyIndividual(node.Xref.Replace("@", string.Empty));
                            family.individuals.Add(individual.Id, individual);
                            individual.AddPersona(persona);
                        }
                        break;

                    case "NOTE":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with top-level NOTE tags
                        break;

                    case "OBJE":
                        // TODO - Doug - 8/5/2006 - parse info out of top-level multimedia OBJEct tags
                        break;

                    case "REPO":
                        // TODO - Doug - 8/5/2006 - parse info out of the top-level REPO record
                        break;

                    case "SOUR":
                        // TODO - Doug - 8/5/2006 - parse info out of the top-level SOUR record
                        break;

                    case "SUBN":
                        // TODO - Doug - 8/5/2006 - parse info out of the top-level SUBN record
                        break;

                    case "SUBM":
                        // TODO - Doug - 8/5/2006 - parse info out of the top-level SUBM record
                        break;

                    case "TRLR":
                        sawTrailer = true;
                        break;

                    default:
                        results.LogUnhandledTag(node);
                        break;
                }
            }

            if (!sawHeader)
            {
                results.LogWarning("No HEAD record was seen.");
            }

            if (!sawTrailer)
            {
                results.LogWarning("No TRLR record was seen.");
            }

            results.SetCounts(family); 

            return family;
        }



        private void LoadExtract(XmlElement extractElement, LoadResults loadResults)
        {
            // Snag the ID and make sure it is valid...
            string id = extractElement.GetAttribute("id");

            if (string.IsNullOrEmpty(id))
            {
                // TODO - Doug - 8/27/2006 - need to get a line number or some other info in here so the user can pinpoint the error...
                loadResults.LogWarning("Extract without ID could not be loaded.");
                return;
            }

            // Snag the source ID and make sure it is valid...
            string sourceId = extractElement.GetAttribute("sourceId");

            if (string.IsNullOrEmpty(sourceId))
            {
                // TODO - Doug - 8/27/2006 - need to get a line number or some other info in here so the user can pinpoint the error...
                loadResults.LogWarning("Extract without source ID could not be loaded.");
                return;
            }

            // TODO - Doug - 9/2/2006 - handle the case where the extract is parsed before the source
            if (!this.sources.ContainsKey(sourceId))
            {
                loadResults.LogWarning("Source ID '{0}' could not be found when parsing extract.", sourceId);
                return;
            }

            FamilySource source = this.sources[sourceId];

            // Create the extract, and parse it...
            FamilyExtract extract = new FamilyExtract(id, source);
            this.extracts.Add(id, extract);

            extract.ParseXml(this, extractElement, loadResults);
        }



        private void LoadIndividual(XmlElement individualElement, LoadResults loadResults)
        {
            // Snag the ID and make sure it is valid...
            string id = individualElement.GetAttribute("id");

            if (string.IsNullOrEmpty(id))
            {
                // TODO - Doug - 8/27/2006 - need to get a line number or some other info in here so the user can pinpoint the error...
                loadResults.LogWarning("Individual without ID could not be loaded.");
                return;
            }

            // TODO - Doug - 8/27/2006 - look to see if this individual has already been created (via a reference)

            FamilyIndividual individual = new FamilyIndividual(id);

            this.individuals.Add(id, individual);

            individual.ParseXml(this, individualElement);
        }



        private void LoadSource(XmlElement sourceElement, LoadResults loadResults)
        {
            // Snag the ID and make sure it is valid...
            string id = sourceElement.GetAttribute("id");

            if (string.IsNullOrEmpty(id))
            {
                // TODO - Doug - 8/27/2006 - need to get a line number or some other info in here so the user can pinpoint the error...
                loadResults.LogWarning("Source without ID could not be loaded.");
                return;
            }

            // TODO - Doug - 8/27/2006 - look to see if this source has already been created (via a reference)

            FamilySource source = new FamilySource(id);

            this.sources.Add(id, source);

            source.ParseXml(this, sourceElement, loadResults);
        }



        private void HandleInclude(XmlElement includeElement, LoadResults loadResults, FileInfo parentInfo)
        {
            // Build up the file name...
            string pathFragment = includeElement.GetAttribute("path");

            if (string.IsNullOrEmpty(pathFragment))
            {
                loadResults.LogWarning("Include element without path attribute could not be parsed.");
                return;
            }

            string fullPath = Path.Combine(parentInfo.DirectoryName, pathFragment);

            FileInfo info = new FileInfo(fullPath);

            if (!info.Exists)
            {
                loadResults.LogWarning("Include file '{0}' does not exist.", fullPath);
                return;
            }

            XmlDocument doc = new XmlDocument();
            doc.Load(info.FullName);

            this.ParseXml(doc, loadResults, info);
        }



        public void ParseXml(XmlDocument doc, LoadResults loadResults, FileInfo info)
        {
            foreach (XmlElement elem in doc.SelectNodes("/family/*"))
            {
                switch (elem.Name)
                {
                    case "extract":
                        this.LoadExtract(elem, loadResults);
                        break;

                    case "individual":
                        this.LoadIndividual(elem, loadResults);
                        break;

                    case "source":
                        this.LoadSource(elem, loadResults);
                        break;

                    case "include":
                        this.HandleInclude(elem, loadResults, info);
                        break;

                    default:
                        loadResults.LogWarning("Unknown top-level element '{0}' found when parsing XML.", elem.Name);
                        break;
                }
            }
        }



        public static FamilyInfo Load(FileInfo info, LoadResults loadResults)
        {
            FamilyInfo family = new FamilyInfo();

            XmlDocument doc = new XmlDocument();
            doc.Load(info.FullName);

            family.ParseXml(doc, loadResults, info);

            loadResults.SetCounts(family);

            return family;
        }



        public void Save(FileInfo info, SaveResults saveResults)
        {
            // Start with a clean slate
            if (info.Exists)
            {
                info.Delete();
            }

            // Dump everything out...
            using (FileStream outputStream = info.OpenWrite())
            {
                XmlWriterSettings xmlSettings = new XmlWriterSettings();
                xmlSettings.Indent = true;
                xmlSettings.IndentChars = "    ";

                using (XmlWriter writer = XmlWriter.Create(outputStream, xmlSettings))
                {
                    writer.WriteStartElement("family");

                    foreach (FamilySource source in this.sources.Values)
                    {
                        source.SaveXml(writer);
                    }

                    foreach (FamilyExtract extract in this.extracts.Values)
                    {
                        extract.SaveXml(writer);
                    }

                    foreach (FamilyIndividual individual in this.individuals.Values)
                    {
                        individual.SaveXml(writer);
                    }

                    writer.WriteEndElement();

                    writer.Close();
                }

                outputStream.Close();
            }
        }




        public Dictionary<string, FamilyIndividual> Individuals
        {
            get { return this.individuals; }
        }



        public Dictionary<string, FamilyPersona> Personas
        {
            get { return this.personas; }
        }



        public Dictionary<string, FamilySource> Sources
        {
            get { return this.sources; }
        }



        public Dictionary<string, FamilyExtract> Extracts
        {
            get { return this.extracts; }
        }



        public override bool Equals(object obj)
        {
            // Simple cases...
            if (obj == null)
            {
                return false;
            }

            FamilyInfo other = obj as FamilyInfo;

            if (other == null)
            {
                return false;
            }

            // TODO - Doug - 9/2/2006 - take out the console.writeline calls; they are here to make the
            // unit test failures easier to track down...

            // Lists should have the same number of items
            if (this.individuals.Count != other.individuals.Count)
            {
                Console.Out.WriteLine("FamilyInfo.Equals: this.individuals.Count != other.individuals.Count...");
                return false;
            }

            if (this.personas.Count != other.personas.Count)
            {
                Console.Out.WriteLine("FamilyInfo.Equals: this.personas.Count != other.personas.Count...");
                return false;
            }

            if (this.extracts.Count != other.extracts.Count)
            {
                Console.Out.WriteLine("FamilyInfo.Equals: this.extracts.Count != other.extracts.Count...");
                return false;
            }

            if (this.sources.Count != other.sources.Count)
            {
                Console.Out.WriteLine("FamilyInfo.Equals: this.sources.Count != other.sources.Count...");
                return false;
            }

            // Go through and compare the personas
            foreach (string key in this.personas.Keys)
            {
                if (!other.personas.ContainsKey(key))
                {
                    return false;
                }

                FamilyPersona thisPersona = this.personas[key];
                FamilyPersona otherPersona = other.personas[key];

                if (!thisPersona.Equals(otherPersona))
                {
                    return false;
                }
            }

            // Go through and compare individuals
            foreach (string key in this.individuals.Keys)
            {
                if (!other.individuals.ContainsKey(key))
                {
                    return false;
                }

                FamilyIndividual thisIndividual = this.individuals[key];
                FamilyIndividual otherIndividual = other.individuals[key];

                if (!thisIndividual.Equals(otherIndividual))
                {
                    return false;
                }
            }

            // Go through and compare the sources
            foreach (string key in this.sources.Keys)
            {
                if (!other.sources.ContainsKey(key))
                {
                    Console.WriteLine("FamilyInfo.Equals: !other.sources.ContainsKey(key)...key='{0}'...", key);
                    return false;
                }

                FamilySource thisSource = this.sources[key];
                FamilySource otherSource = other.sources[key];

                if (!thisSource.Equals(otherSource))
                {
                    Console.WriteLine("FamilyInfo.Equals: !thisSource.Equals(otherSource)...key='{0}'...", key);
                    return false;
                }
            }

            // Go through and compare the extracts
            foreach (string key in this.extracts.Keys)
            {
                if (!other.extracts.ContainsKey(key))
                {
                    Console.WriteLine("FamilyInfo.Equals: !other.extracts.ContainsKey(key)...key='{0}'...", key);
                    return false;
                }

                FamilyExtract thisExtract = this.extracts[key];
                FamilyExtract otherExtract = other.extracts[key];

                if (!thisExtract.Equals(otherExtract))
                {
                    Console.WriteLine("FamilyInfo.Equals: !thisExtract.Equals(otherExtract)...key='{0}'...", key);
                    return false;
                }
            }

            return true;
        }


        public override int GetHashCode()
        {
            // TODO - Doug - 8/14/2006 - do we need to do something better here?  Perhaps hashing on name?
            return base.GetHashCode();
        }
    }
}
